﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Windows.Shapes;
using System.Windows.Threading;
using System.Windows.Media.Imaging;

using System.Text;
using System.Windows.Media.Effects;

namespace GameEngine
{
    public delegate void LayerContentLoadedHandler(object sender, LayerContentLoadedArgs e);

    /// <summary>
    ///     Arguments for the tile LayerContentLoaded events.
    /// </summary>
    public class LayerContentLoadedArgs : EventArgs
    {
        //to be added
    }

    public delegate void TriggerHandler(object sender, TriggerArgs e);
    //public delegate void WalkOverTriggerHandler(object sender, TriggerArgs e);
    //public delegate void FlyOverTriggerHandler(object sender, TriggerArgs e);


    /// <summary>
    ///     Arguments for the tile trigger events.
    /// </summary>
    public class TriggerArgs : EventArgs
    {
        //to be added
    }


    public enum TileTriggerTypes
    {
        onclick,
        onwalkover,
        onflyover
    }


    public enum TileLayers
    {
        foreground, 
        background,
        both
    }


    /// <summary>
    ///     States the tile can have.
    /// </summary>
    public enum TileStates
    {
        //tile is blank
        blank,
        //tile is transparent
        transparent, 
        //normal tile with image 
        tile
    }


    /// <remarks>
    ///     Holds the info about one tile- its visual and triggers.
    /// </remarks>
    public class Tile
    {
        /// <summary>
        ///     Bitmap holding the background of the tile.
        /// </summary>
        private WriteableBitmap _background;
        /// <summary>
        ///     Bitmap holding the foreground of the tile.
        /// </summary>
        private WriteableBitmap _foreground;
        /// <summary>
        ///     Path to the tileset from which the foreground tile is.
        /// </summary>
        public string _foregroundSource;
        /// <summary>
        ///     Path to the tileset from which the background tile is.
        /// </summary>
        public string _backgroundSource;
        /// <summary>
        ///     Transform used when loading background.
        /// </summary>
        public Transform _backgroundTransform;
        /// <summary>
        ///     Transform used when loading foreground.
        /// </summary>
        public Transform _foregroundTransform;
        /// <summary>
        ///     Bitmap holding the combinaton of all layers from down-to-top-background, foreground, objects.
        /// </summary>
        private WriteableBitmap _final;
        /// <summary>
        ///     Rectangle for blitting.
        /// </summary>
        private Rect _rectangle;
        /// <summary>
        ///     Holds the information whether the tile was changed.
        /// </summary>
        private bool _changed = true;
        /// <summary>
        ///     State of this tile's foreground layer.
        /// </summary>
        private TileStates _foregroundState = TileStates.transparent;
        /// <summary>
        ///     State of this tile's background layer.
        /// </summary>
        private TileStates _backgroundState = TileStates.transparent;
        /// <summary>
        ///     True if foreground has been loaded.
        /// </summary>
        private bool _foregroundLoaded = false;
        /// <summary>
        ///     True if background has been loaded.
        /// </summary>
        private bool _backgroundLoaded = false;
        /// <summary>
        ///     Effect used on the foreground layer.
        /// </summary>
        private Effect _foregroundEffect = null;
        /// <summary>
        ///     Effect used on the background layer.
        /// </summary>
        private Effect _backgroundEffect = null;
        /// <summary>
        ///     Used for updating animation.
        /// </summary>
        private double _elapsedSeconds = 0.0;
        /// <summary>
        ///     True if the animation has been cloned-can now run independently.
        /// </summary>
        private bool _animationCloned = false; 


        /// <summary>
        ///     True if the tile is selected.
        /// </summary>
        private bool _selected = false;
        /// <summary>
        ///     Gets/Sets if the tile is selected.
        /// </summary>
        public bool Selected
        {
            get
            {
                return _selected;
            }
            set
            {
                _selected = value;
                //enforeces redraw of this tile
                this._changed = true;

                if (_selected == true && this._selectionRect == null)
                {
                    this._selectionRect = new WriteableBitmap(this._tileSize.Width, this._tileSize.Height);

                    //clears the tile to transparent
                    this._selectionRect.Clear(Colors.Transparent);

                    Color c = Colors.Red;

                    //makes the red border to visually differentiate selected tile
                    for (int x = 0; x < this._tileSize.Width; x++)
                    {
                        //top
                        this._selectionRect.SetPixel(x, 0, c);
                        //bottom
                        this._selectionRect.SetPixel(x, this._tileSize.Height - 1, c);
                    }

                    for (int y = 0; y < this._tileSize.Height; y++)
                    {
                        //left
                        this._selectionRect.SetPixel(0, y, c);
                        //right
                        this._selectionRect.SetPixel(this._tileSize.Width-1, y, c);
                    }
                }
            }
        }

        /// <summary>
        ///     Bitmap holding the image of selection rectangle. Created when the selected is set.
        /// </summary>
        private WriteableBitmap _selectionRect;


        /// <summary>
        ///     Controls which layer is shown.
        /// </summary>
        private TileLayers _show = TileLayers.both;
        /// <summary>
        ///     Gets/Sets which layer is shown. Redraws the tile.
        /// </summary>
        public TileLayers Show
        {
            get
            {
                return _show;
            }
            set
            {
                _show = value;
                _changed = true;
            }
        }

        /// <summary>
        ///     If the tile should be considered for pathfinding.
        /// </summary>
        private bool _pathable = true;
        /// <summary>
        ///     Gets/sets the information whether the tile should be considered for pathfinding.
        /// </summary>
        public bool Pathable
        {
            get
            {
                return _pathable;
            }
            set
            {
                _pathable = value;
            }
        }

        /// <summary>
        ///     If the tile should be considered for fly-pathfinding.
        /// </summary>
        private bool _flyPathable = true;
        /// <summary>
        ///     Gets/sets the information whether the tile should be considered for fly-pathfinding.
        /// </summary>
        public bool FlyPathable
        {
            get
            {
                return _flyPathable;
            }
            set
            {
                _flyPathable = value;
            }
        }


        /// <summary>
        ///     Holds the size of the tile;
        /// </summary>
        private IntSize _tileSize = new IntSize(0, 0);
        /// <summary>
        ///     Gets/Sets the size of the tile.
        /// </summary>
        public IntSize TileSize
        {
            get
            {
                return _tileSize;
            }
            set
            {
                _tileSize = value;
            }
        }

        /// <summary>
        ///     Represents the animation for animated tiles.
        /// </summary>
        private Spritesheet _animation=null;
        /// <summary>
        ///     Sets the animation for animated tiles.
        /// </summary>
        public Spritesheet Animation
        {
            get
            {
                return _animation;
            }
            set 
            {
                _animation = value;
            }
        }

        /// <summary>
        ///     
        /// </summary>
        private double _frameLength;
        /// <summary>
        /// 
        /// </summary>
        public double FrameLength
        {
            get 
            {
                return _frameLength;
            }
            set
            {
                _frameLength = value;
            }
        }


        /// <summary>
        ///     Fires when the foreground layer content is loaded.
        /// </summary>
        public event LayerContentLoadedHandler ForegroundLayerContentLoaded;
        /// <summary>
        ///     Fires when the background layer content is loaded.
        /// </summary>
        public event LayerContentLoadedHandler BackgroundLayerContentLoaded;

        /// <summary>
        ///     Fires the event. Event is fired when the content(be it blank, transparent or image) is loaded.
        /// </summary>
        /// <param name="layer">Which layer's content has been loaded.</param>
        public virtual void OnLayerContentLoaded(TileLayers layer)
        {
            if (layer == TileLayers.background)
            {
                if (ForegroundLayerContentLoaded != null)
                    ForegroundLayerContentLoaded(this, new LayerContentLoadedArgs());
            }
            else if (layer == TileLayers.foreground)
            {
                if (BackgroundLayerContentLoaded != null)
                    BackgroundLayerContentLoaded(this, new LayerContentLoadedArgs());
            }

        }


        /// <summary>
        ///     Fires when the user clicks on the tile.
        /// </summary>
        public event TriggerHandler ClickTrigger;
        /// <summary>
        ///     Fires the event. Event is fired externally from GameGrid.
        /// </summary>
        /// <param name="e"></param>
        public virtual void OnClickTrigger(TriggerArgs e)
        {
            if (ClickTrigger != null)
                ClickTrigger(this, e);
        }


        /// <summary>
        ///     Fires when the user clicks on the tile.
        /// </summary>
        public event TriggerHandler WalkOverTrigger;
        /// <summary>
        ///     Fires the event. Event is fired externally from GameGrid.
        /// </summary>
        /// <param name="e"></param>
        public virtual void OnWalkOverTrigger(TriggerArgs e)
        {
            if (WalkOverTrigger != null)
                WalkOverTrigger(this, e);
        }


        /// <summary>
        ///     Fires when the user clicks on the tile.
        /// </summary>
        public event TriggerHandler FlyOverTrigger;
        /// <summary>
        ///     Fires the event. Event is fired externally from GameGrid.
        /// </summary>
        /// <param name="e"></param>
        public virtual void OnFlyOverTrigger(TriggerArgs e)
        {
            if(FlyOverTrigger != null)
                FlyOverTrigger(this, e);
        }



        /// <summary>
        ///     Constructs a tile with size <paramref name="size"/>.  
        /// </summary>
        /// <param name="size">Size of the tile constructed.</param>
        public Tile(IntSize size)
        {
            //size.Height = size.Height * 2;
            //size.Width = size.Width * 2;

            //stores the size
            _tileSize = new IntSize(size.Width, size.Height);
            //creates the bitmaps
            _background = new WriteableBitmap(_tileSize.Width, _tileSize.Height);
            _foreground = new WriteableBitmap(_tileSize.Width, _tileSize.Height);
            _final = new WriteableBitmap(_tileSize.Width, _tileSize.Height);
            _rectangle = new Rect(0, 0, _tileSize.Width, _tileSize.Height);

            //handler which sets the loaded properties
            this.ForegroundLayerContentLoaded += new LayerContentLoadedHandler(Foreground_LayerContentLoaded);
            this.BackgroundLayerContentLoaded += new LayerContentLoadedHandler(Background_LayerContentLoaded);
        }


        /// <summary>
        ///     Sets loaded to true on the foreground layer.
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        void Foreground_LayerContentLoaded(object sender, LayerContentLoadedArgs e)
        {
            this._foregroundLoaded = true;
        }


        /// <summary>
        ///     Sets loaded to true on the background layer.
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        void Background_LayerContentLoaded(object sender, LayerContentLoadedArgs e)
        {
            this._backgroundLoaded = true;
        }


        /// <summary>
        ///     Constructs a tile with size <paramref name="size"/> converted to integer.  
        /// </summary>
        /// <param name="size">Size of the tile constructed.</param>
        public Tile(Size size): this(new IntSize((int)size.Width, (int)size.Height))
        {
        }


        /// <summary>
        ///     Returns the string representation of this tile.
        /// </summary>
        /// <returns></returns>
        public string StringRepresentation()
        {
            StringBuilder temp = new StringBuilder();

            //: stands for no value for paths to files, 1 for coordinates when the source is null
            if (this._animation == null)
            {
                if (_foregroundState == TileStates.blank)
                {
                    temp.Append("B 1 1");
                }
                else if (_foregroundState == TileStates.transparent)
                {
                    temp.Append("T 1 1");
                }
                else
                {
                    temp.Append(this._foregroundSource + " " + ((TranslateTransform)this._foregroundTransform).X + " " + ((TranslateTransform)this._foregroundTransform).Y);
                }

                if (_backgroundState == TileStates.blank)
                {
                    temp.Append(" B 1 1");
                }
                else if (_backgroundState == TileStates.transparent)
                {
                    temp.Append(" T 1 1");
                }
                else
                {
                    temp.Append(" " + this._backgroundSource + " " + ((TranslateTransform)this._backgroundTransform).X + " " + ((TranslateTransform)this._backgroundTransform).Y);
                }
            }
            else
            {
                temp.Append("S " + this._animation.StringRepresentation() + " 1 S " + this._animation.StringRepresentation() + " 1");
            }

            if (this.Pathable)
                temp.Append(" T ");
            else
                temp.Append(" F ");
            
            if (this.FlyPathable)
                temp.Append("T");
            else
                temp.Append("F");

            return temp.ToString();
        }


        /// <summary>
        ///     Sets this tile as blank. 
        /// </summary>
        /// <param name="layer">The layer to be set blank.</param>
        public void SetBlankTile(TileLayers layer)
        {
            WriteableBitmap temp = new WriteableBitmap(this._tileSize.Width, this._tileSize.Height);

            //clears the tile to white
            temp.Clear(Colors.White);

            Color c = Colors.Black;

            //makes the black border to visually separate tiles
            for (int x = 0; x < this._tileSize.Width; x++)
            {
                //top
                temp.SetPixel(x, 0, c);
                //bottom
                temp.SetPixel(x, this._tileSize.Height - 1, c);
            }

            for (int y = 0; y < this._tileSize.Height; y++)
            {
                //left
                temp.SetPixel(0, y, c);
                //right
                temp.SetPixel(this._tileSize.Width - 1, y, c);
            }

            switch (layer)
            {
                case TileLayers.background:
                    this._background = temp;
                    this._backgroundSource = null;
                    this._backgroundTransform = null;
                    this._backgroundState = TileStates.blank;
                    this.OnLayerContentLoaded(TileLayers.background);
                    break;
                case TileLayers.foreground:
                    this._foreground = temp;
                    this._foregroundSource = null;
                    this._foregroundTransform = null;
                    this._foregroundState = TileStates.blank;
                    this.OnLayerContentLoaded(TileLayers.foreground);
                    break;
            }

            this._changed = true;
        }


        /// <summary>
        ///     Sets this tile as transparent. 
        /// </summary>
        /// <param name="layer">The layer to be set transparent.</param>
        public void SetTransparentTile(TileLayers layer)
        {
            WriteableBitmap temp = new WriteableBitmap(this._tileSize.Width, this._tileSize.Height);

            //clears the tile to white
            temp.Clear(Colors.Transparent);

            switch (layer)
            {
                case TileLayers.background:
                    this._background = temp;
                    this._backgroundSource = null;
                    this._backgroundTransform = null;
                    this._backgroundState = TileStates.transparent;
                    this.OnLayerContentLoaded(TileLayers.background);
                    break;
                case TileLayers.foreground:
                    this._foreground = temp;
                    this._foregroundSource = null;
                    this._foregroundTransform = null;
                    this._foregroundState = TileStates.transparent;
                    this.OnLayerContentLoaded(TileLayers.foreground);
                    break;
            }

            this._changed = true;
        }


        /// <summary>
        ///     Sets the foreground/background of the tile.
        /// </summary>
        /// <param name="tileset">URI of the tileset used.</param>
        /// <param name="transform">How should be the image transformed before setting it as foreground/background.</param>
        /// <param name="layer">Foreground/Background</param>
        public void SetTileImage(String tileset, Transform transform, TileLayers layer)
        {
            if (layer == TileLayers.background)
            {
                this._backgroundSource = String.Copy(tileset);
                this._backgroundState = TileStates.tile;
            }
            else if (layer == TileLayers.foreground)
            {
                this._foregroundSource = String.Copy(tileset);
                this._foregroundState = TileStates.tile;
            }

            BitmapImage tilesetImage = new BitmapImage(new Uri(tileset,UriKind.RelativeOrAbsolute));
            this.SetTileImage(tilesetImage, transform, layer);
        }


        /// <summary>
        ///     Sets the foreground/background of the tile.
        /// </summary>
        /// <param name="tileset">BitmapImage with the tileset used.</param>
        /// <param name="transform">How should be the image transformed before setting it as foreground/background.</param>
        /// <param name="layer">Foreground/Background</param>
        public void SetTileImage(BitmapImage tileset, Transform transform, TileLayers layer)
        {
            //creates a new image holder
            Image image = new Image();
            //hides the image - we don't need to see it
            image.Visibility = Visibility.Collapsed;
            //and adds it to the visual tree so we can use the imageopened event
            (GameEngine.App.Current.RootVisual as MainPage).LayoutRoot.Children.Add(image);

            if (layer == TileLayers.background)
            {
                //set up the handler
                image.ImageOpened += new EventHandler<RoutedEventArgs>(LoadBackground);
                this._backgroundTransform = transform;
            }
            else if (layer == TileLayers.foreground)
            {
                //set up the handler
                image.ImageOpened += new EventHandler<RoutedEventArgs>(LoadForeground);
                this._foregroundTransform = transform;
            }

            //set the image's source
            image.Source = tileset;            
        }


        /// <summary>
        ///     Loads the background image into the bitmap.    
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void LoadBackground(object sender, RoutedEventArgs e)
        {
            Image image = (sender as Image);

            this._background.Clear(Colors.Transparent);
            this._background.Render(image, this._backgroundTransform);
            this._background.Invalidate();
            this._changed = true;

            this.OnLayerContentLoaded(TileLayers.background);

            //removes the image from the visual tree-we do not need it anymore
            (GameEngine.App.Current.RootVisual as MainPage).LayoutRoot.Children.Remove(image);
        }


        /// <summary>
        ///     Loads the foreground image into the bitmap.    
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void LoadForeground(object sender, RoutedEventArgs e)
        {
            Image image = (sender as Image);

            this._foreground.Clear(Colors.Transparent);
            this._foreground.Render(image, this._foregroundTransform);
            this._foreground.Invalidate();
            this._changed = true;

            this.OnLayerContentLoaded(TileLayers.foreground);

            //removes the image from the visual tree-we do not need it anymore
            (GameEngine.App.Current.RootVisual as MainPage).LayoutRoot.Children.Remove(image);
        }


        /// <summary>
        ///     Sets an effect for the desired layer. 
        ///     Effect is applied only after the layer is set.
        /// </summary>
        /// <param name="effect">Effect to be set.</param>
        /// <param name="layer">
        /// Layer where the effect should be aplied. 
        /// Only background or foreground, not both.
        /// </param>
        public void SetEffect(Effect effect, TileLayers layer)
        {
            //loads the effect now if the layer is already set
            if ((layer == TileLayers.background && this._backgroundLoaded == true) || (layer == TileLayers.foreground && this._foregroundLoaded == true))
            {
                LoadEffect(effect, layer);
            }
            //loads effect after the layer is set
            else
            {
                if (layer == TileLayers.foreground)
                {
                    this._foregroundEffect = effect;
                    this.ForegroundLayerContentLoaded += new LayerContentLoadedHandler(LoadForegroundEffect);
                }
                else if (layer == TileLayers.background)
                {
                    this._backgroundEffect = effect;
                    this.BackgroundLayerContentLoaded += new LayerContentLoadedHandler(LoadBackgroundEffect);
                }
            }
        }


        /// <summary>
        ///     Loads the effect for foreground layer.
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void LoadForegroundEffect(object sender, LayerContentLoadedArgs e)
        {
            LoadEffect(this._foregroundEffect, TileLayers.foreground);
            this.ForegroundLayerContentLoaded -= new LayerContentLoadedHandler(LoadForegroundEffect);
        }


        /// <summary>
        ///     Loads the effect for background layer.
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void LoadBackgroundEffect(object sender, LayerContentLoadedArgs e)
        {
            LoadEffect(this._backgroundEffect, TileLayers.background);
            this.BackgroundLayerContentLoaded -= new LayerContentLoadedHandler(LoadBackgroundEffect);
        }


        /// <summary>
        ///     Loads effect and applies it on the selected layer.
        /// </summary>
        /// <param name="effect">Effect to be loaded.</param>
        /// <param name="layer">Effect is apllied to this layer.</param>
        private void LoadEffect(Effect effect, TileLayers layer)
        {
            if (layer == TileLayers.background)
            {
                this._background = EffectHelper.ApplyEffect(this._background, effect);
            }
            else if (layer == TileLayers.foreground)
            {
                this._foreground = EffectHelper.ApplyEffect(this._foreground, effect);
            }
            
            this._changed = true;
        }


        /// <summary>
        ///     Changes frames for this tile based on the elapsed time.
        /// </summary>
        /// <param name="elapsedTime"></param>
        public void Animate(TimeSpan elapsedTime)
        {
            _elapsedSeconds += elapsedTime.TotalSeconds;
            if (_animation != null && _animation.Ready == true && (_elapsedSeconds > this._frameLength))
            {
                if (!_animationCloned)
                {
                    this._animation = Spritesheet.Clone(this._animation);
                    _animationCloned = true;
                }
                _final = this._animation.Next();
                _elapsedSeconds = 0;
                this._changed = true;
            }
        }


        /// <summary>
        ///     Returns the visual representation of background+foreground on the tile.
        /// </summary>
        /// <returns>Image of tile as a bitmap.</returns>
        public WriteableBitmap GetTileImage()
        {
            //only updates the bitmap when something changes and the tile is not animated
            if (_changed && (_animation == null || !(_animation.Ready)))
            {
                //_final = new WriteableBitmap(_tileSize.Width, _tileSize.Height);
                //_final.Clear(Colors.Transparent);
                switch (this._show)
                {
                    case TileLayers.background:
                        _final.Blit(_rectangle, _background, _rectangle, WriteableBitmapExtensions.BlendMode.None);
                        break;
                    case TileLayers.foreground:
                        _final.Blit(_rectangle, _foreground, _rectangle, WriteableBitmapExtensions.BlendMode.None);
                        break;
                    case TileLayers.both:
                        _final.Blit(_rectangle, _background, _rectangle, WriteableBitmapExtensions.BlendMode.None);
                        _final.Blit(_rectangle, _foreground, _rectangle, WriteableBitmapExtensions.BlendMode.AlphaBlend);
                        break;
                }
                
                _changed = false;

            }

            //when the tile is selected
            if (_selected == true)
                _final.Blit(_rectangle, _selectionRect, _rectangle, WriteableBitmapExtensions.BlendMode.AlphaBlend);

            return _final;
        }

    }
}
